閱讀本篇文章前,仔細想想看
- 迭代器(Iterator)與聚合物(Collection)的差別在哪?
- 迭代器模式要如何實踐?實踐的目的為何?
- 什麼是多型巡訪(Polymorphic Iteration)?
如果還不清楚可以看一下前一篇文章喔~
今天的主題對於任何本系列的讀者應該是很重要的篇章,相信讀者也是早就碰過 ES2015+ 的語法,但是想要再進修才會學習原生 JavaScript 結合 TypeScript 的型別系統下去。
tsconfig.json
裡的 lib
設定裡新增 es2015
這已經在專案編譯篇章裡面已經講過了。
反正就是在 tsconfig.json
裡,將 es2015
選項新增到 lib
設定裡喔。
{
"compileOptions": {
"lib": ["dom", "es2015"]
}
}
Map
& Set
筆者先從比較單純的東西講,Map
跟 Set
在 TypeScript 的泛型用法。
貼心小提示
這裡筆者就預設讀者已經知道 ES6
Map
與Set
的用法囉~但是如果沒有用過的話可以看這裡:另外,沒有用過的讀者可能會疑惑,為何要講這個東西 —— 理由以下筆者就會解釋。
ES6 Map
是用來儲存鍵值對的物件,跟普通的 JSON 物件感覺很像,但是差別在於:
重點 1. ES6 Map V.S. JSON Object
ES6
Map
可以使用任何型別(不局限於字串)的值作為鍵(key);但普通的 JSON 物件 —— 儘管看似可以使用字串和數字作為鍵,但任何型別的值作為 JSON 物件的鍵(key)都會被轉換成字串型態。
然後,因為 Map
可以使用任何型別的值作為鍵,因此這裡就是泛用之型別參數可以代表的地方;不過,理所當然地,Map
裡的鍵所對應到的值也可以作為另一個型別參數 —— 也就是說,泛用的 Map
本身有兩個型別參數分別代表鍵與值的型別。(以下程式碼是可以正常使用的,讀者可以自行試試看)
另外,筆者想要強調,善用型別系統的推論部分 —— 比如,如果臨時忘記 Map.prototype.set
方法要填的參數與對應型別,TypeScript 會自動跟你提示。(如圖一)
圖一:使用 Map.prototype.set
時,TypeScript 會彈出視窗提示
另一個筆者認為很好用的資料結構為 ES6 提供的 Set
:
重點 2. ES6
Set
與普通的列表狀結構(如:Array)的差別
Set
為數學上集合的一種體現,因此代表內部所存取的元素符合:
- 無序性:沒有任何順序可言
- 互異性:沒有任何元素重複,每個元素互為不同
- 確定性:元素不外乎只有存在於或者是不存在於集合裡面,這兩種情形
但普通的陣列:
- 可以存在重複的元素
- 內部的元素具有順序性
而 ES6
Set
使用起來比起陣列可以更輕易地進行不同Set
物件的聯集(union
)、差集(difference
)或交集(intersection
)。
理所當然地,我們可以提供型別參數的值代表 Set<T>
內部存的元素型別。
重點 3. 泛用
Map
與Set
物件若建構一個 ES6
Map
型別的物件,建議提供兩個型別參數的值分別代表Map<Tkey, Tvalue>
的鍵與值的對應型別。若建構一個 ES6
Set
型別的物件,建議提供一個型別參數的值代表Set<T>
內部元素所存取的型別。
讀者試試看
如果是沒有提供型別參數的情況下,以下的
unspecifiedTypeMap1
與unspecifiedTypeSet1
分別的推論結果為何?但是如果是以下的情形,
unspecifiedTypeMap2
與unspecifiedTypeSet2
分別的推論型別為何?裡面有無初始值會影響推論結果嗎?
Promise
物件事實上,之前在模擬戰 —— UBike 篇章有稍微使用過 Promise
物件了,不過暫且還是簡介一下:
重點 4. ES6
Promise
物件的用意與目的
Promise
物件可以針對非同步的事件(Asynchronous Events)進行狀態機(State Machine)式的編程操作,狀態有:
pending
:當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態,pending
意指等待內部的程式碼的結果resolved
:Promise 內部的非同步過程執行成功時的結果rejected
:Promise 內部的非同步過程執行失敗時的結果對於 Promise 有任何問題,社群上有很多熱心的人們已經把 Promise 講到不能筆者覺得不能再講下去了 XD,而且 MDN Document 也寫得清清楚楚。
貼心小提示
筆者還是再三強調一次:當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態;這也代表不管你後續有沒有串接
then
或catch
方法,Promise
在被建構的那一刻就已經開始在執行內部的程式碼!
通常簡單的 Promise 物件程式碼可能會長這樣,
sendRequest
本身是非同步的 Action(當然,簡單一點的如:setTimeout
等也是非同步的行為),而如果執行過程有結果就會根據不同的 response.status
結果判斷該 Promise
為 resolved
還是 rejected
狀態。
而後 request
存的 Promise
物件分別對 resolved
/rejected
狀態有不同的後續處理方式。
事實上,更好一點的理解形式,筆者就直接畫出圖來。(如圖二)
圖二:Promise
可看作是針對非同步事件進行狀態機的表示形式
不過 Promise
的功用還有很多,像是如果是 resolved
的狀態時,在 [Promise Object].then
裡的回呼函式如果回傳的是另一個 Promise
物件,我們可以不停地一直串聯下去。
這樣的行為用更完整的圖會是這樣的呈現。(如圖三)
圖三:更完整的 Promise
運作圖,就連你在 catch
錯誤時,依然可以選擇回傳新的 Promise
物件持續這個狀態機的迴圈下去喔!
回過頭來,Promise
在 TypeScript 裡依然跟泛型的使用有關 —— 也就是 Promise<Tresolved>
—— 你可以提供一個型別值代表當 Promise
進入 resolved
的狀態時的結果的值之型別。
以下筆者就寫個簡單範例:
當你特別註記 Promise<string>
就代表:resolve()
內部的值必須填入 string
型別,如果不是的話就會顯示錯誤訊息如圖四。
圖四:顯示數字 200
不為 string | PromiseLike<string> | undefined
型別
其實光是錯誤訊息就透露 —— 連 undefined
也就是空值可以視為 resolve
可填入的東西,至於 PromiseLike<string>
可以想成可以填入類似 Promise.resolve('Succeeded')
這種東西。
不過筆者依然想不透什麼情形會寫成 resolve(Promise.resolve('Succeeded'))
。
另外,如果你註記為 Promise<string>
時,使用該 Promise
物件的 Promise.prototype.then
方法則是會提示參數的型別。(如圖五)
圖五:then
裡面的提示
乍看之下裡面內容挺恐怖的,但是整理過後仔細瞧瞧:
好,還是很亂 XDD,但慢慢來解析。
onfulfilled?
為選用屬性,可以填入一個函式型別 —— 該函式的參數為 value: string
,跟原本的 Promise<string>
裡提供的 string
型別參數的值連結。而輸出部分則除了 string
與 Nullable Types 以外,還可以填入 PromiseLike<string>
,也就是指出剛剛筆者在圖三時有提到的:Promise
物件可以串下去的行為。
另外的 onrejected?
則是當 Promise
物件裡的非同步程式碼執行有出錯或者是被使用者主動呼叫 reject
的話觸發的行為。裡面也可以填入函式型別,其參數型別為 reason: any
,這一點之所以沒有跟 Promise
物件的型別參數進行連結的原因,可想而知,錯誤的出現形式可不會只是字串而已,光是物件的組合也挺多種,因此才是少數會用到 any
型別的情形。
至於 PromiseLike<never>
就是指這個 Promise 本身無法完整地執行完畢,直接拋出錯誤的概念。(參見 never
型別篇章)
同理,Promise.prototype.catch
方法裡的敘述跟 onrejected?
在 Promise.prototype.then
差不多。(如圖六)
圖六:Promise.prototype.catch
的提示內容
重點 5. 泛用 ES6 Promise 物件
在 TypeScript 的世界裡,
Promise<Tresolved>
為Promise
物件的完整泛用表示式。而型別參數Tresolved
代表Promise
物件時,呼叫resolve
方法可以填入的型別外,還代表未來在使用Promise.prototype.then
方法時,回呼函式的參數代表之型別。此外,
resolve
時除了可為Tresolved
型別外,也可以是空值(undefined
)以及類似於PromiseLike<string>
的型別之值。
貼心小提示
讀者應該還是會感到疑惑,為何有所謂的
xxLike
型別的東西 —— 比如ArrayLike<T>
或PromiseLike<T>
等。提示:跟 TypeScript
lib
設定有關。由於這個是進階性的話題,筆者還是給讀者一帖 StackOverflow 提問做補充。
讀者試試看
這幾題比起剛剛的
Map
與Set
還要來得重要,請讀者務必要親手驗證過以下的行為。
- 請問如果我們不提供型別參數的值給
Promise
物件,以下的unspecifiedTypePromise
的推論型別為何?
- 但如果假設,
Promise
物件裡的resolve
函式被呼叫時有填入特定型別之值,則unspecifiedTypePromise
此時的推論結果為何?
請讀者根據題目 1 與 2 的結果推論:
Promise<T>
中,T
必須主動註記的必要性 —— 我們是否應當積極註記Promise<T>
而非Promise
而已?如果是直接用
Promise.resolve
或Promise.reject
,請問個別的推論結果為何?若讀者對於
unknown
型別有問題的話,請參見any
v.s.unknown
型別篇章。
筆者以下再測試幾個不同常見的 Promise
物件的功能。
Promise.all
—— 當所有的 Promise
進入 resolved
狀態時執行Promise.all
的概念有點像是很多不同的 Promise
在同一個時刻開始運行,直到所有在 Promise.all
內部的 Promise
都成功 resolve
—— Promise.all([ ... ]).then
才會被執行。
以下的範例程式碼,Promise.all(...)
的推論結果為 Promise<[string, number, boolean]>
—— 該型別參數代表的是元組型別。(如圖七)
圖七:以上的程式碼,Promise.all
此時的推論結果
如果刻意將其中一個故意 reject
掉,儘管看似應該要回傳類似 Promise<never>
這種會出現錯誤的狀態,但它仍然會按照元組格式去顯示結果。(如圖八,不過這種行為依筆者來看應該是錯的)
圖八:儘管很明顯筆者刻意要用 Promise.reject
讓 Promise.all
壞掉,但事實上它還是會顯示元組型別格式的推論結果
Promise.race
—— 所有的 Promise
進行比賽,誰先 resolve
誰就獲勝這邊很明顯應該不會是用元組型別來顯示結果,而是會用 union
複合型別方式呈現推論結果,因為 Promise.race
裡的每個 Promise
都有被 resolve
的可能。(以下範例程式碼推論結果如圖九)
圖九:推論結果為 Promise<string | number | boolean>
以上的程式碼,筆者只是簡簡單單地建構一個 delay<T>
函式,目的是將 Promise
延緩幾個時間 resolve
。
Promise.race
通常好用的地方在於實現 Request Timeout 功能:
比如說,你有一個 arbitraryRequest
是為一個 Promise
(或 PromiseLike
)物件,但是你希望這個請求能夠在三秒內處理完畢,如果沒有就 reject
掉,你可以使用 Promise.race
並且將該請求跟一個計時器比賽 —— 如果計時器獲勝則代表該 Promise
可以執行 reject
過後的狀態。
筆者在本篇大概講最多的應該是 Promise<T>
這個物件的型別以及推論機制與結果,讀者應該也從這一篇發現泛用型別的重要性了吧~
下一篇筆者要緊接著正進階的部分 —— 泛用型別與 ES2015+ 非同步語法的結合應用喔~